using System;
using System.Reflection;
using System.Xml;
using System.Xml.Serialization;
using Td.Additional.ComponentModel;

namespace Td.Additional.Xml
{
    /// <summary>
    /// Xml helper class.
    /// </summary>
    public static class Xml
    {
        #region Private static descriptors

        internal const String _TypeDescriptor = "_type";
        internal const String _ObjectIdDescriptor = "_id";

        #endregion

        #region Common xml facilities

        /// <summary>
        /// Creates a new XML document.
        /// </summary>
        /// <param name="RootTagName">Name of the root tag.</param>
        /// <returns>The new XML document</returns>
        public static XmlDocument CreateXmlDocument(String RootTagName)
        {
            XmlDocument result = new XmlDocument();
            result.AppendChild(result.CreateXmlDeclaration("1.0", null, null));
            result.AppendChild(CreateXmlElement(result, RootTagName, ""));
            return result;
        }

        /// <summary>
        /// Creates the xml element.
        /// </summary>
        /// <param name="doc">The xml document.</param>
        /// <param name="Name">The tag name.</param>
        /// <param name="InnerText">The inner text.</param>
        /// <returns>The xml element.</returns>
        public static XmlElement CreateXmlElement(XmlDocument doc, String Name, String InnerText)
        {
            XmlElement element = doc.CreateElement(Name);
            if (!String.IsNullOrEmpty(InnerText))
                element.InnerText = InnerText;
            return element;
        }

        /// <summary>
        /// Sets the XML attribute.
        /// </summary>
        /// <param name="element">The element.</param>
        /// <param name="name">The name.</param>
        /// <param name="value">The value.</param>
        /// <returns></returns>
        public static XmlElement SetXmlAttribute(XmlElement element, String name, String value)
        {
            element.SetAttribute(name, value);
            return element;
        }

        /// <summary>
        /// Sets the XML attribute.
        /// </summary>
        /// <param name="node">The node.</param>
        /// <param name="name">The name.</param>
        /// <param name="value">The value.</param>
        /// <returns></returns>
        public static XmlNode SetXmlAttribute(XmlNode node, String name, String value)
        {
            var attribute = node.OwnerDocument.CreateAttribute(name);
            attribute.Value = value;
            node.Attributes.Append(attribute);
            return node;
        }

        /// <summary>
        /// Gets the attribute.
        /// </summary>
        /// <param name="node">The node.</param>
        /// <param name="name">The name.</param>
        /// <returns></returns>
        public static String GetAttribute(XmlNode node, String name)
        {
            XmlNode attrNode = node.Attributes.GetNamedItem(name);
            if (attrNode == null)
                return String.Empty;
            return attrNode.InnerText;
        }

        #endregion

        #region Write properties to xml

        /// <summary>
        /// Writes the properties to XML.
        /// </summary>
        /// <param name="doc">The doc.</param>
        /// <param name="propertyContainer">The property container.</param>
        public static void WritePropertiesToXml(XmlDocument doc, object propertyContainer)
        {
            WritePropertiesToXml(doc.DocumentElement, propertyContainer);
        }

        /// <summary>
        /// Writes the properties to XML.
        /// </summary>
        /// <param name="element">The element.</param>
        /// <param name="propertyContainer">The property container.</param>
        public static void WritePropertiesToXml(XmlElement element, object propertyContainer)
        {
            XmlWritePropertiesProcess xmlPropertyProcess = new XmlWritePropertiesProcess();
            xmlPropertyProcess.AddReferenceObject(propertyContainer);
            WritePropertiesToXml(xmlPropertyProcess, element, propertyContainer);
        }

        /// <summary>
        /// Writes the properties to XML.
        /// </summary>
        /// <param name="xmlPropertyProcess">The XML property process.</param>
        /// <param name="element">The element.</param>
        /// <param name="propertyContainer">The property container.</param>
        private static void WritePropertiesToXml(XmlWritePropertiesProcess xmlPropertyProcess, XmlElement element, object propertyContainer)
        {
            Type type = propertyContainer.GetType();
            foreach (PropertyInfo info in type.GetProperties())
                WritePropertyToXml(xmlPropertyProcess, element, propertyContainer, info.Name, info.PropertyType);
        }

        /// <summary>
        /// Writes the property to XML.
        /// </summary>
        /// <param name="xmlPropertyProcess">The XML property process.</param>
        /// <param name="node">The node.</param>
        /// <param name="propertyContainer">The property container.</param>
        /// <param name="fieldName">Name of the field.</param>
        /// <param name="fieldType">Type of the field.</param>
        /// <returns></returns>
        private static Boolean WritePropertyToXml(XmlWritePropertiesProcess xmlPropertyProcess, XmlNode node, object propertyContainer, String fieldName, Type fieldType)
        {
            try
            {
                // TODO: Check for reference cycle.
                Type type = propertyContainer.GetType();
                PropertyInfo propertyInfo = type.GetProperty(fieldName);
                object[] attr = propertyInfo.GetCustomAttributes(typeof(XmlIgnoreAttribute), true);
                if (attr.Length > 0)
                    return true;
                var asAttribute = propertyInfo.GetCustomAttributes(typeof(XmlAttributeAttribute), true);
                if (fieldType.IsGenericType)
                {
                    if (asAttribute.Length > 0)
                        throw new InvalidOperationException("A generic list cannot serialized as a xml attribute. Remove the XmlAttributeAttribute or redeclare the property " + propertyInfo.Name);
                    object list = type.InvokeMember(fieldName, BindingFlags.GetProperty, null, propertyContainer, null);
                    if (list != null)
                    {
                        int count = (int)list.GetType().InvokeMember("Count", BindingFlags.GetProperty, null, list, null);
                        attr = type.GetCustomAttributes(typeof(IgnoreEmptyListsAttribute), true);
                        if (attr.Length > 0 && count == 0)
                            return true;
                        XmlElement nodeList = (XmlElement)node.AppendChild(CreateXmlElement(node.OwnerDocument, fieldName, null));
                        attr = propertyInfo.GetCustomAttributes(typeof(ReferenceListAttribute), true);
                        var isReferenceList = attr.Length > 0;
                        if (isReferenceList)
                            for (int i = 0; i < count; i++)
                                xmlPropertyProcess.AddReferenceObject(
                                list.GetType().InvokeMember("Item", BindingFlags.GetProperty, null, list, new object[] { i }));
                        for (int i = 0; i < count; i++)
                        {
                            // TODO: Attend XmlRootAttribute.
                            XmlNode subNode = nodeList.AppendChild(CreateXmlElement(node.OwnerDocument, fieldType.GetGenericArguments()[0].Name + i.ToString(), null));
                            object item = list.GetType().InvokeMember("Item", BindingFlags.GetProperty, null, list, new object[] { i });
                            if (!item.GetType().Equals(fieldType.GetGenericArguments()[0]))
                                Xml.SetXmlAttribute((XmlElement)subNode, _TypeDescriptor, item.GetType().ToString());
                            if (!isReferenceList)
                                if (!xmlPropertyProcess.AddObject(item, (XmlElement)subNode))
                                    continue;
                            WritePropertiesToXml(xmlPropertyProcess, (XmlElement)subNode, item);
                        }
                    }
                }
                else
                {
                    object item = type.InvokeMember(fieldName, BindingFlags.GetProperty, null, propertyContainer, null);
                    if (item != null)
                    {
                        if (item.GetType().IsClass && !fieldType.Namespace.StartsWith("System"))
                        {
                            if (asAttribute.Length > 0)
                                throw new InvalidOperationException("A class cannot be serialized as a xml attribute. Remove the XmlAttributeAttribute or redeclare the property " + propertyInfo.Name);
                            attr = propertyInfo.GetCustomAttributes(typeof(XmlRootAttribute), true);
                            if (attr.Length > 0)
                                fieldName = (attr[0] as XmlRootAttribute).ElementName;
                            XmlNode subNode = node.AppendChild(CreateXmlElement(node.OwnerDocument, fieldName, null));
                            if (!item.GetType().Equals(fieldType))
                                Xml.SetXmlAttribute((XmlElement)subNode, _TypeDescriptor, item.GetType().ToString());
                            if (xmlPropertyProcess.AddObject(item, (XmlElement)subNode))
                                WritePropertiesToXml(xmlPropertyProcess, (XmlElement)subNode, item);
                        }
                        else
                        {
                            try
                            {
                                if (item is int || item is double || item is float)
                                    if (item.Equals(Convert.ChangeType(0, fieldType)))
                                        return true;
                                if (item is String && item.ToString() == String.Empty)
                                    return true;
                                if (item is DateTime && DateTime.Parse(item.ToString()) == DateTime.MinValue)
                                    return true;
                                if (item is TimeSpan && TimeSpan.Parse(item.ToString()) == TimeSpan.MinValue)
                                    return true;
                            }
                            catch (FormatException ex)
                            {
                                Debug.ConsoleReport("Invalid format for '{0}': {1}", item, ex.Message);
                            }
                            catch (OverflowException ex)
                            {
                                Debug.ConsoleReport("'{0}' is invalid: {1}", item, ex.Message);
                            }
                            catch (InvalidCastException ex)
                            {
                                Debug.ConsoleReport("Cannot convert '{0}' to '{1}': {2}", item, fieldType, ex.Message);
                            }
                            if (asAttribute.Length > 0)
                            {
                                var xaa = asAttribute[0] as XmlAttributeAttribute;
                                SetXmlAttribute(node, String.IsNullOrEmpty(xaa.AttributeName) ? fieldName : xaa.AttributeName, item.ToString());
                            }
                            else
                                node.AppendChild(CreateXmlElement(node.OwnerDocument, fieldName, item.ToString()));
                        }
                    }
                }
                return true;
            }
            catch (Exception ex)
            {
                Debug.ConsoleReport(ex.Message);
                xmlPropertyProcess.Exceptions.Add(ex);
                return false;
            }
        }

        #endregion

        #region Load properties from xml

        /// <summary>
        /// Loads the properties from XML.
        /// </summary>
        /// <param name="doc">The doc.</param>
        /// <param name="propertyContainer">The property container.</param>
        /// <returns></returns>
        public static object LoadPropertiesFromXml(XmlDocument doc, object propertyContainer)
        {
            return LoadPropertiesFromXml(doc.DocumentElement, propertyContainer);
        }

        /// <summary>
        /// Loads the properties from XML.
        /// </summary>
        /// <param name="element">The element.</param>
        /// <param name="propertyContainer">The property container.</param>
        /// <returns></returns>
        public static object LoadPropertiesFromXml(XmlElement element, object propertyContainer)
        {
            XmlReadPropertiesProcess xmlReadPropertiesProcess = new XmlReadPropertiesProcess();
            return LoadPropertiesFromXml(xmlReadPropertiesProcess, element, propertyContainer);
        }

        /// <summary>
        /// Loads the properties from XML.
        /// </summary>
        /// <param name="xmlReadPropertiesProcess">The XML read properties process.</param>
        /// <param name="element">The element.</param>
        /// <param name="propertyContainer">The property container.</param>
        /// <returns></returns>
        private static object LoadPropertiesFromXml(XmlReadPropertiesProcess xmlReadPropertiesProcess, XmlElement element, object propertyContainer)
        {
            foreach (PropertyInfo info in propertyContainer.GetType().GetProperties())
                LoadPropertyFromXml(
                    xmlReadPropertiesProcess,
                    element.SelectSingleNode("child::" + info.Name),
                    propertyContainer, info.Name, info.PropertyType);
            return propertyContainer;
        }

        /// <summary>
        /// Gets the type from string.
        /// </summary>
        /// <param name="typeName">Name of the type.</param>
        /// <returns></returns>
        private static Type GetTypeFromString(String typeName)
        {
            Type type = Type.GetType(typeName);
            if (type == null)
                foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
                {
                    type = assembly.GetType(typeName);
                    if (type != null)
                        break;
                }
            return type;
        }

        /// <summary>
        /// Loads the property from XML.
        /// </summary>
        /// <param name="xmlReadPropertiesProcess">The XML read properties process.</param>
        /// <param name="node">The node.</param>
        /// <param name="propertyContainer">The property container.</param>
        /// <param name="name">The property name.</param>
        /// <param name="fieldType">Type of the property field.</param>
        /// <returns>
        /// True, if property is successfully loaded.
        /// </returns>
        private static Boolean LoadPropertyFromXml(XmlReadPropertiesProcess xmlReadPropertiesProcess, XmlNode node, object propertyContainer, String name, Type fieldType)
        {
            if (node == null || propertyContainer == null)
                return false;
            try
            {
                Type propertyContainerType = propertyContainer.GetType();
                object dest = null;
                if (fieldType.IsGenericType)
                {
                    Type[] subTypes = fieldType.GetGenericArguments();
                    object list = propertyContainerType.InvokeMember(name, BindingFlags.GetProperty, null, propertyContainer, null);
                    if (list == null)
                        list = propertyContainerType.InvokeMember(name, BindingFlags.SetProperty, null, propertyContainer, new object[] { fieldType.InvokeMember("", BindingFlags.CreateInstance, null, null, null) });
                    object[] attr = propertyContainerType.GetProperty(name).GetCustomAttributes(typeof(ReferenceListAttribute), true);
                    var isReferenceList = attr.Length > 0;// List contains reference objects ?
                    foreach (XmlNode childNode in node.ChildNodes)
                    {
                        object newElement = null;
                        var idStr = GetAttribute(childNode, _ObjectIdDescriptor);
                        var typeStr = GetAttribute(childNode, _TypeDescriptor);
                        if (!String.IsNullOrEmpty(idStr) && !String.IsNullOrEmpty(typeStr))
                        {// Search reference object
                            Type type = GetTypeFromString(typeStr);
                            if (type != null)
                                xmlReadPropertiesProcess.AddObject(new XmlObjectInfo(type, propertyContainer, idStr, propertyContainerType.GetProperty(name)));
                            continue;
                        }
                        else
                            if (!String.IsNullOrEmpty(typeStr))
                            {
                                Type type = GetTypeFromString(typeStr);
                                newElement = type.InvokeMember("", BindingFlags.CreateInstance, null, null, null);
                            }
                            else
                                newElement = subTypes[0].InvokeMember("", BindingFlags.CreateInstance, null, null, null);
                        newElement = LoadPropertiesFromXml(xmlReadPropertiesProcess, (XmlElement)childNode, newElement);
                        list.GetType().InvokeMember("Add", BindingFlags.InvokeMethod, null, list, new object[] { newElement });
                        if (isReferenceList)
                            xmlReadPropertiesProcess.AddReferenceObject(newElement);
                    }
                }
                else
                {
                    var idStr = GetAttribute(node, _ObjectIdDescriptor);
                    var typeStr = GetAttribute(node, _TypeDescriptor);
                    var type = fieldType;
                    if (!String.IsNullOrEmpty(typeStr))
                        type = GetTypeFromString(typeStr) ?? fieldType;
                    if (fieldType.IsEnum)
                        dest = Enum.Parse(fieldType, node.InnerText);
                    else
                        if (type.IsClass && !type.Namespace.StartsWith("System"))
                        {
                            if (!String.IsNullOrEmpty(idStr) && !String.IsNullOrEmpty(typeStr))
                            {
                                if (type != null)
                                    xmlReadPropertiesProcess.AddObject(new XmlObjectInfo(type, propertyContainer, idStr, propertyContainerType.GetProperty(name)));
                                return true;
                            }
                            if (!String.IsNullOrEmpty(typeStr))
                            {
                                dest = type.InvokeMember("", BindingFlags.CreateInstance, null, null, null);
                            }
                            else
                                dest = fieldType.InvokeMember("", BindingFlags.CreateInstance, null, null, null);
                            LoadPropertiesFromXml(xmlReadPropertiesProcess, (XmlElement)node, dest);
                            foreach (XmlAttribute attribute in node.Attributes)
                            {
                                var propertyInfo = type.GetProperty(attribute.Name);
                                if (propertyInfo == null)
                                    foreach (var fieldPropertyInfo in type.GetProperties())
                                    {
                                        var attr = fieldPropertyInfo.GetCustomAttributes(typeof(XmlAttributeAttribute), true);
                                        XmlAttributeAttribute xaa = attr.Length > 0 ? (XmlAttributeAttribute)attr[0] : null;
                                        if (xaa != null && xaa.AttributeName == attribute.Name)
                                        {
                                            propertyInfo = fieldPropertyInfo;
                                            break;
                                        }
                                    }
                                if (propertyInfo != null)
                                    type.InvokeMember(
                                        propertyInfo.Name,
                                        BindingFlags.SetProperty,
                                        null,
                                        dest,
                                        new object[] { Convert.ChangeType(attribute.Value, propertyInfo.PropertyType) });
                            }
                        }
                        else
                            dest = Convert.ChangeType(node.InnerText, fieldType);
                    propertyContainerType.InvokeMember(name, BindingFlags.SetProperty, null, propertyContainer, new object[] { dest });
                }
                return true;
            }
            catch (Exception ex)
            {
                xmlReadPropertiesProcess.Exceptions.Add(ex);
                Debug.ConsoleReport(ex.Message);
                return false;
            }
        }

        /// <summary>
        /// Loads the properties from XML.
        /// </summary>
        /// <param name="doc">The doc.</param>
        /// <param name="propertyContainerType">Type of the property container.</param>
        /// <returns></returns>
        public static object LoadPropertiesFromXml(XmlDocument doc, Type propertyContainerType)
        {
            Object propertyContainer = propertyContainerType.InvokeMember(String.Empty, BindingFlags.CreateInstance, null, null, null);
            return LoadPropertiesFromXml(doc, propertyContainer);
        }

        #endregion
    }
}